大家好,歡迎來到第二十九天!在 Day 28,我們學習了 Cursor + Figma MCP 的 AI 驅動設計開發一體化。今天,我們要面對一個關鍵問題:App 效能。
在開發 Crew Up! 的過程中,我們遇到了一些效能痛點:
今天我們要分享實際在 Crew Up! 專案中完成的 7 項效能優化,以及具體的效果測量。
在開始優化前,我們先用 Flutter DevTools 分析了整個 App:
遇到的問題:
在測試時發現,每次進入個人檔案頁面,大頭貼都要重新下載一次。用 DevTools 的 Network 分析後發現:
解決方案:
1. 安裝 cached_network_image 套件
dependencies:
cached_network_image: ^3.4.1
2. 建立統一的快取圖片 Widget
// lib/common/widgets/cached_image_widget.dart
// (imports omitted)
class CachedImageWidget extends StatelessWidget {
final String imageUrl;
final double? width;
final double? height;
final BoxFit fit;
final int? memCacheWidth;
final int? memCacheHeight;
@override
Widget build(BuildContext context) {
return CachedNetworkImage(
imageUrl: imageUrl,
width: width,
height: height,
fit: fit,
memCacheWidth: memCacheWidth,
memCacheHeight: memCacheHeight,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
);
}
}
3. 更新 ImageProviderUtils
// lib/common/utils/image_provider_utils.dart
// (imports omitted)
static ImageProvider getImageProvider(String imagePath) {
if (imagePath.startsWith('http')) {
return CachedNetworkImageProvider(imagePath);
}
return AssetImage(imagePath);
}
💡 專家提示:記憶體快取尺寸的黃金法則
在使用 CachedNetworkImage
時,memCacheWidth
和 memCacheHeight
參數至關重要:
CachedNetworkImage(
imageUrl: url,
width: 250,
memCacheWidth: 500, // 2x for Retina screens
)
為什麼要設為顯示尺寸的兩倍?
memCacheWidth
和 memCacheHeight
是以像素為單位。將其設定為顯示尺寸的兩倍,是為了讓圖片在記憶體中以更高解析度快取。這樣一來,當在高 DPI(如 Retina、2x/3x 螢幕)上顯示時,圖片能保持清晰銳利,避免因拉伸而變得模糊。
舉例來說:
memCacheWidth: 500
確保在高 DPI 螢幕上依然清晰實際優化效果:
如何驗證:
遇到的問題:
每個活動卡片都在 build() 方法中計算倒數時間,當列表有 20 個活動時,每次重建就要計算 20 次。
解決方案:
使用 ActivityCountdownProvider
,由 Provider 統一管理倒數計時,每分鐘自動更新一次。
// lib/features/home/presentation/widgets/activity_card_widget.dart
// (imports omitted)
class ActivityCardWidget extends ConsumerWidget {
final Activity activity;
@override
Widget build(BuildContext context, WidgetRef ref) {
final countdownState = ref.watch(
activityCountdownNotifierProvider(activity.registrationDeadline),
);
return Text(countdownState.displayText);
}
}
實際優化效果:
遇到的問題:
列表滾動時 FPS 只有 45-50。檢查後發現:ListView 項目沒有 key,Flutter 無法正確重用 Widget。
解決方案:
改用 ListView.builder
並為每個項目加上 ValueKey
。
// lib/features/home/presentation/screens/activity_list_screen.dart
// (imports omitted)
Widget _buildActivityList(ActivityListState state) {
if (state.activities.isEmpty) {
return EmptyStateWidget(
title: S.of(context).noActivities,
cubiMessage: S.of(context).emptyPageStory,
);
}
return ListView.builder(
itemCount: state.activities.length,
itemBuilder: (context, index) {
final activity = state.activities[index];
return ActivityCardWidget(
key: ValueKey(activity.id), // 關鍵!讓 Flutter 正確重用
activity: activity,
onTap: () => _onActivityTap(activity),
);
},
);
}
實際優化效果:
遇到的問題:
首頁的背景圖片很大(2032 × 1500),每次進入都會看到圖片載入閃爍。
解決方案:
在首頁初始化時使用 precacheImage()
提前載入。
// lib/features/home/presentation/screens/index_screen.dart
// (imports omitted)
class _IndexScreenState extends ConsumerState<IndexScreen> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(
const AssetImage('assets/index/index_background.png'),
context,
);
}
}
💡 為什麼是 didChangeDependencies
而不是 initState
?
許多開發者可能會疑惑:為什麼不放在 initState
中?這個選擇展現了對 Flutter 生命週期的深刻理解:
initState
:在 Widget 第一次被插入 Widget 樹時調用,此時 Widget 還沒有完全初始化,使用 context
可能會有問題didChangeDependencies
:在 initState
之後、build
之前被調用,此時 Widget 已經完全插入到 Widget 樹中,可以安全地使用 context
因為 precacheImage()
需要一個有效的 BuildContext
來存取 Theme、MediaQuery 等資訊,所以 didChangeDependencies
是執行預快取的理想時機。
實際優化效果:
遇到的問題:
聊天室是 Crew Up! 中效能問題最明顯的地方:
解決方案:
參考 WhatsApp、Telegram 等主流聊天 App,採用三個關鍵技術:
技術 1:使用 reverse: true ListView
// lib/features/message/presentation/widgets/message_chat_messages_widget.dart
// (imports omitted)
@override
Widget build(BuildContext context) => ListView.builder(
controller: scrollController,
reverse: true, // 關鍵!最新訊息在 position 0(底部)
cacheExtent: 500,
itemCount: messages.length,
itemBuilder: (context, index) {
final message = messages[messages.length - 1 - index];
return _MessageBubble(
key: ValueKey(message.id),
message: message,
);
},
);
技術 2:將訊息氣泡改為獨立 Widget
// ❌ 舊方式:方法
Widget _buildMessageBubble(Message message) {
return Row(...); // 每次都重建,無法被快取
}
// ✅ 新方式:獨立 Widget
class _MessageBubble extends StatelessWidget {
final Message message;
@override
Widget build(BuildContext context) {
// Flutter 可以正確快取和重用這個 Widget
}
}
技術 3:簡化滾動邏輯
// lib/features/message/presentation/screens/message_chat_screen.dart
// (imports omitted)
void _scrollToBottom() {
if (!_scrollController.hasClients) return;
final isNearBottom = _scrollController.position.pixels < 200;
if (isNearBottom) {
_scrollController.animateTo(
0, // reverse: true 時,0 是最新訊息
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
為什麼 reverse: true 這麼神奇?
比較項目 | 正常順序 | reverse: true |
---|---|---|
初始位置 | 需要滾動到底部 | 自動在底部 ✅ |
渲染效能 | 需計算全部高度 | 只渲染可見區域 ✅ |
代碼複雜度 | 高 | 低 ✅ |
實際優化效果:
遇到的問題:
首頁有兩個水平滾動列表(熱門活動、我的活動),滾動時也有輕微卡頓。
解決方案:
為水平列表的項目加上 ValueKey
。
// lib/features/home/presentation/widgets/popular_activity_list_widget.dart
// (imports omitted)
ListView.separated(
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return PopularActivityCardWidget(
key: ValueKey(activities[index].id),
activity: activities[index],
);
},
)
實際優化效果:
遇到的問題:
在 DevTools 中,我們注意到即使沒有數據變化,某些靜態的 UI 組件(如標題、提示文字、空狀態頁面)有時也會跟著父組件一起被重建(rebuild),造成輕微的效能浪費。
解決方案:
對於那些創建後就不會再改變的 Widget,在建構函式前加上 const
。這會告訴 Flutter,這個 Widget 是不可變的。在下次 build 時,如果父組件觸發重建,Flutter 會直接跳過這個 const
Widget,甚至不會調用它的 build 方法。
1. 將 EmptyStateWidget 改為 const
// lib/common/widgets/empty_state_widget.dart
// (imports omitted)
class EmptyStateWidget extends StatelessWidget {
final String title;
final String cubiMessage;
const EmptyStateWidget({ // 將建構函式改為 const
super.key,
required this.title,
required this.cubiMessage,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(title),
Text(cubiMessage),
],
);
}
}
2. 使用時也加上 const
// lib/features/home/presentation/screens/activity_list_screen.dart
// (imports omitted)
Widget _buildActivityList(ActivityListState state) {
if (state.activities.isEmpty) {
return const EmptyStateWidget( // 調用時也加上 const
title: '沒有活動',
cubiMessage: '快來創建一個吧!',
);
}
// ...
}
💡 const 的強大之處
當您標記一個 Widget 為 const
時:
const
Widget 共享同一個記憶體實例實際優化效果:
const
Widget 子樹的重建工作const
Widget 只占用一份記憶體const
而跳過重建的部分變多了適用場景:
一開始以為效能問題在演算法或狀態管理,結果用 DevTools 發現:圖片載入是最大的效能瓶頸!
觀察到的現象:
改用 cached_network_image
後,整個 App 的感覺完全不同。
關鍵學習:優化要從最大的瓶頸下手,用工具觀察而不是憑感覺猜測。
最初花了很多時間寫複雜的滾動邏輯,結果發現業界標準做法就是用 reverse: true
,一行程式碼解決所有問題!
關鍵學習:遇到複雜邏輯時,先研究看看業界是怎麼做的。
只需要一行程式碼:
key: ValueKey(item.id)
就能讓 Flutter 正確重用 Widget,減少大量不必要的重建。
關鍵學習:所有動態列表都應該加 key,這是必須的基本優化。
# | 優化項目 | 修改檔案 | 核心技術 | 觀察到的效果 |
---|---|---|---|---|
1 | 圖片快取 | 7 個 | cached_network_image |
網路流量大幅減少,載入明顯變快 |
2 | 倒數計時 | 1 個 | ConsumerWidget + Provider |
CPU 使用更穩定 |
3 | 垂直列表 | 1 個 | ValueKey + ListView.builder |
滾動更流暢,掉幀減少 |
4 | 背景預快取 | 1 個 | precacheImage() + didChangeDependencies |
首頁無閃爍,載入更快 |
5 | 聊天室 | 2 個 | reverse: true + 獨立 Widget |
初始化瞬間,滾動流暢 |
6 | 水平列表 | 2 個 | ValueKey |
水平滾動更順暢 |
7 | const 優化 | 多個 | const 關鍵字 |
CPU 使用降低,記憶體減少 |
總計:修改 14+ 個檔案,新增 1 個檔案,新增 1 個套件
如何自己測量:
flutter run --profile
運行// ❌ 沒有 key
ActivityCard(activity: activity)
// ✅ 有 key
ActivityCard(
key: ValueKey(activity.id),
activity: activity,
)
適用於所有動態列表:ListView、GridView、聊天訊息等。
ListView.builder(
reverse: true,
cacheExtent: 500,
itemBuilder: (context, index) {
final item = items[items.length - 1 - index];
return Widget(key: ValueKey(item.id), data: item);
},
)
黃金法則:顯示尺寸 × 2
CachedNetworkImage(
imageUrl: url,
width: 250,
memCacheWidth: 500, // 2x for Retina screens
)
@override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(const AssetImage('large_bg.png'), context);
}
// 建構函式加上 const
class MyWidget extends StatelessWidget {
const MyWidget({super.key, required this.title});
final String title;
// ...
}
// 使用時加上 const
const MyWidget(title: 'Hello')
// 適用場景:所有參數都是編譯時常數的 Widget
# 使用 Profile 模式測試
flutter run --profile
測試項目 | 如何驗證 | 預期結果 |
---|---|---|
列表滾動 | 開啟 Performance Overlay | 綠色條為主,紅色條很少 |
圖片快取 | 查看 DevTools Network | 第二次載入無網路請求 |
首頁載入 | 觀察背景圖片 | 無閃爍,立即顯示 |
聊天室 | 進入聊天室 | 立即看到最新訊息 |
今天我們在 Crew Up! 專案中完成了 7 項效能優化,涵蓋了圖片、列表、狀態管理、滾動、Widget 重建等各個面向。透過 Flutter DevTools 的測量和觀察,成功將 App 從「卡頓」優化到「流暢」。
從這次優化中,我們學到四個最重要的技術:
明天(Day 30),我們將探討 Flutter 30 天挑戰的完整回顧與總結,回顧這段精彩的學習旅程,分享 Crew Up 專案的完整成果。
期待與您在 Day 30 相見!